package com.java.architect.manager.aop;

import com.java.architect.entity.InventoryId;
import com.java.architect.operator.InventoryOperator;
import com.java.architect.utils.CacheUtils;
import com.java.architect.utils.OperatorResult;
import com.java.architect.utils.RedissonLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@Aspect
@Component
@SuppressWarnings({"rawtypes","unchecked"})
public class InventoryAspect {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Resource
    RedissonLock redissonLock;

    @Pointcut("@target(com.java.architect.manager.aop.Inventory)")
    public void pointCut() {}

    //execution(<修饰符模式>？<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
    @Around(value = "execution(public * com.java.architect.operator.InventoryOperator+.*(..)) && pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        final Class declaringType = signature.getDeclaringType();
        String invokeName = signature.getMethod().getName();
        Inventory inventory = AnnotationUtils.getAnnotation(declaringType, Inventory.class);
        InventoryOperator operator = (InventoryOperator)joinPoint.getTarget();

        if(invokeName.equals("getTotal")){
            return this.getTotal(operator,inventory, (InventoryId) joinPoint.getArgs()[0]);
        }
        if(invokeName.equals("getUsed")){
            return this.getUsed(operator,inventory, (InventoryId) joinPoint.getArgs()[0]);
        }
        if(invokeName.equals("deduct")){
            return this.deduct(operator,inventory, (InventoryId) joinPoint.getArgs()[0], (Long) joinPoint.getArgs()[1]);
        }
        if(invokeName.equals("release")){
            return this.release(operator,inventory, (InventoryId) joinPoint.getArgs()[0], (Long) joinPoint.getArgs()[1]);
        }
        if(invokeName.equals("flushUsed")){
            return this.flushUsed(operator,inventory, (InventoryId) joinPoint.getArgs()[0],(long)joinPoint.getArgs()[1]);
        }

        return joinPoint.proceed();
    }

    private OperatorResult deduct(InventoryOperator operator, Inventory inventory, InventoryId id, long cnt){
        long total = this.getTotal(operator,inventory,id);
        long used = this.getUsed(operator,inventory,id);
        if(used >= total){
            //库存不足
            return new OperatorResult(false,0);
        }

        String inventoryKey = CacheUtils.getInventoryUsedKey(inventory,id);
        used = stringRedisTemplate.opsForValue().increment(inventoryKey,cnt);
        if(used > total){
            //库存不足 回滚redis数据
            stringRedisTemplate.opsForValue().decrement(inventoryKey,cnt);
            return new OperatorResult(false,0);
        }
        if(used == total){
            //最后一个库存扣减完毕，立即刷新数据到数据库
            this.flushUsed(operator,inventory,id,used);
        }
        return new OperatorResult(true,used);
    }

    private OperatorResult release(InventoryOperator operator, Inventory inventory, InventoryId id, long cnt){
        long used = this.getUsed(operator,inventory,id);
        if(used <= 0){
            //库存不足
            return new OperatorResult(false,0);
        }
        String inventoryKey = CacheUtils.getInventoryUsedKey(inventory,id);
        used = stringRedisTemplate.opsForValue().decrement(inventoryKey,cnt);
        if(used < 0 ){
            stringRedisTemplate.opsForValue().increment(inventoryKey,cnt);
            //库存不足
            return new OperatorResult(false,0);
        }
        return new OperatorResult(true,used);
    }

    private boolean flushUsed(InventoryOperator operator, Inventory inventory, InventoryId id,long inv){
        String inventoryKey = CacheUtils.getInventoryUsedKey(inventory,id);
        stringRedisTemplate.opsForValue().set(inventoryKey,String.valueOf(inv));
        if(inventory.flushToDb()){
            return operator.flushUsed(id,inv);
        }
        return true;
    }

    private long getTotal(InventoryOperator operator, Inventory inventory, InventoryId id){
        if(inventory.ignoreTotalCache()){
            return operator.getTotal(id);
        }
        String totalKey = CacheUtils.getInventoryTotalKey(inventory,id);
        String totalStr = stringRedisTemplate.opsForValue().get(totalKey);
        long total = 0;
        if(StringUtils.isEmpty(totalStr)){
            String lockKey = CacheUtils.getInventoryLockKey(inventory,id);
            if(redissonLock.tryLock(lockKey,200, TimeUnit.MILLISECONDS)){
                try {
                    totalStr = stringRedisTemplate.opsForValue().get(totalKey);
                    if(StringUtils.isEmpty(totalStr)){
                        total = operator.getTotal(id);
                        if(total > 0){
                            stringRedisTemplate.opsForValue().set(totalKey,String.valueOf(total),inventory.totalExpire(), TimeUnit.SECONDS);
                        }
                    } else {
                        total = Long.parseLong(totalStr);
                    }
                }finally {
                    redissonLock.unlock(lockKey);
                }
            }else{
                throw new RuntimeException("请求过于频繁，请稍后重试");
            }
        } else {
            total = Long.parseLong(totalStr);
        }
        return total;
    }

    private long getUsed(InventoryOperator operator, Inventory inventory, InventoryId id){
        String inventoryKey = CacheUtils.getInventoryUsedKey(inventory,id);
        String usedStr = stringRedisTemplate.opsForValue().get(inventoryKey);
        long used = 0;
        if(StringUtils.isEmpty(usedStr)){
            String lockKey = CacheUtils.getInventoryLockKey(inventory,id);
            if(redissonLock.tryLock(lockKey,200, TimeUnit.MILLISECONDS)){
                try {
                    usedStr = stringRedisTemplate.opsForValue().get(inventoryKey);
                    if(StringUtils.isEmpty(usedStr)){
                        used = operator.getUsed(id);
                        stringRedisTemplate.opsForValue().set(inventoryKey,String.valueOf(used));
                    } else {
                        used = Long.parseLong(usedStr);
                    }
                }finally {
                    redissonLock.unlock(lockKey);
                }
            }else{
                throw new RuntimeException("请求过于频繁，请稍后重试");
            }
        } else {
            used = Long.parseLong(usedStr);
        }
        return used;
    }



}
